/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

import * as cp from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import { TestingCacheSalts } from '../../base/salts';
import { CacheScope } from '../../base/simulationContext';
import { cleanTempDirWithRetry, createTempDir } from '../stestUtil';
import { IFile, ITestDiagnostic } from './diagnosticsProvider';
import { CachingDiagnosticsProvider, findIfInstalled, setupTemporaryWorkspace } from './utils';

/**
 * Class which finds roslyn diagnostics after compilation of C# files
 */
export class RoslynDiagnosticsProvider extends CachingDiagnosticsProvider {

	override readonly id = 'roslyn';
	override readonly cacheSalt = TestingCacheSalts.roslynCacheSalt;
	override readonly cacheScope = CacheScope.Roslyn;

	private _isInstalled: 'local' | 'docker' | false | undefined;

	private get csprojFile(): string {
		return [
			'<Project Sdk="Microsoft.NET.Sdk">',
			'	<PropertyGroup>',
			'		<OutputType>Library</OutputType>',
			'		<TargetFramework>net7.0</TargetFramework>',
			'		<ImplicitUsings>enable</ImplicitUsings>',
			'		<Nullable>enable</Nullable>',
			'		<AllowUnsafeBlocks>true</AllowUnsafeBlocks>',
			'		<ErrorLog>error_list.sarif,version=2.1</ErrorLog>',
			'		<CodeAnalysisRuleSet>CSharp.ruleset</CodeAnalysisRuleSet>',
			'	</PropertyGroup>',
			'	<ItemGroup>',
			'		<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.2.1"/>',
			'		<PackageReference Include="System.Runtime.Loader" Version="4.0.0-*"/>',
			'	</ItemGroup>',
			'</Project>',
		].join("\n");
	}

	private get rulesetFile(): string {
		return [
			'<?xml version="1.0" encoding="utf-8" ?>',
			'<RuleSet Name="CSharp Ruleset" Description="Code analysis rules for CSharp project" ToolsVersion="14.0">',
			'	<Rules AnalyzerId="Microsoft.CodeAnalysis.CSharp" RuleNamespace="Microsoft.CodeAnalysis.CSharp">',
			'		<Rule Id="CS8981" Action="None"/>',
			'	</Rules>',
			'</RuleSet>',
		].join("\n");
	}

	override isInstalled(): boolean {
		if (this._isInstalled === undefined) {
			if (findIfInstalled({ command: 'dotnet', arguments: ['--version'] }, /^(\d+[\.\d+]*)/g)) {
				this._isInstalled = 'local';
			} else if (findIfInstalled({ command: 'docker', arguments: ['--version'] }, /\d+\.\d+\.\d+/)) {
				this._isInstalled = 'docker';
			} else {
				this._isInstalled = false;
			}
		}
		return this._isInstalled !== false;
	}

	protected override async computeDiagnostics(_files: IFile[]): Promise<ITestDiagnostic[]> {
		if (!this.isInstalled()) {
			throw new Error('clang or dotnet must be available in this environment for csharp diagnostics.');
		}
		const temporaryDirectory = await createTempDir();
		try {
			return await this.runDotnetCompiler(temporaryDirectory, _files);
		} finally {
			await cleanTempDirWithRetry(temporaryDirectory);
		}
	}

	private runInDocker(temporaryDirectory: string, basename: string, command: string[]): string {
		const args = ['run', '--rm', '-v', `${temporaryDirectory}:/${basename}`, 'mcr.microsoft.com/dotnet/sdk:8.0', ...command];
		//console.log('docker ' + args.map(arg => `'${arg}'`).join(' '));
		const spawnResult = cp.spawnSync('docker', args, { shell: true, encoding: 'utf-8' });
		if (spawnResult.status !== 0) {
			throw new Error(`Error while running '${command.join(' ')}' in docker : ${spawnResult.stdout} , ${spawnResult.stderr}`);
		}
		return spawnResult.stdout;
	}

	private runInLocal(command: string[]): string {
		const spawnResult = cp.spawnSync(command[0], command.slice(1), { shell: true, encoding: 'utf-8' });
		if (spawnResult.status !== 0) {
			throw new Error(`Error while running '${command.join(' ')}' in local OS : ${spawnResult.stderr}`);
		}
		return spawnResult.stdout;
	}

	private async runDotnetCompiler(temporaryDirectory: string, files: IFile[]): Promise<ITestDiagnostic[]> {

		const projectName = 'project123';
		const projectLocation = path.join(temporaryDirectory, projectName);
		await setupTemporaryWorkspace(projectLocation, files);
		let outputLocation;
		if (this._isInstalled === 'docker') {
			const script = [
				`set -x`,
				`cd "$(dirname "$0")"`, // cd to the directory of the script
				`dotnet new classlib --force -o ${projectName}`,
				`cp CSharp.ruleset ${projectName}/CSharp.ruleset`,
				`cp proj.csproj ${projectName}/${projectName}.csproj`, // replace the csproj file
				`dotnet build ${projectName} --no-incremental`,
				`cp ${projectName}/error_list.sarif error_list.sarif`, // the error_list.sarif file is generated by dotnet build and has the output
				`rm -rfd ${projectName}`, // Docker might run as root, so clean up all generated files right away, we won't be able to do it from the outside
			].join('\n');
			await fs.promises.writeFile(path.join(temporaryDirectory, `validate.sh`), script);
			await fs.promises.writeFile(path.join(temporaryDirectory, `error_list.sarif`), ''); // pre-create the file so it gets the permissions of the current user and we can delete it after
			await fs.promises.writeFile(path.join(temporaryDirectory, `CSharp.ruleset`), this.rulesetFile);
			await fs.promises.writeFile(path.join(temporaryDirectory, `proj.csproj`), this.csprojFile);

			const basename = path.basename(temporaryDirectory);
			this.runInDocker(temporaryDirectory, basename, ['/bin/sh', `/${basename}/validate.sh`]);
			outputLocation = temporaryDirectory;
		} else {
			this.runInLocal(['dotnet', 'new', 'classlib', '--force', '-o', projectLocation]);
			await fs.promises.writeFile(path.join(projectLocation, `${projectName}.csproj`), this.csprojFile);
			await fs.promises.writeFile(path.join(projectLocation, `CSharp.ruleset`), this.rulesetFile);
			this.runInLocal(['dotnet', 'build', projectLocation, '--no-incremental']);
			outputLocation = projectLocation;
		}
		const fileContents = await fs.promises.readFile(path.join(outputLocation, `error_list.sarif`), 'utf8');
		const parsedErrors = JSON.parse(fileContents).runs[0].results;
		const diagnostics: ITestDiagnostic[] = [];
		for (const error of parsedErrors) {
			const uri = error.locations[0].physicalLocation.artifactLocation.uri;
			const diagnostic = {
				file: path.basename(uri),
				message: error.message.text,
				code: error.ruleId,
				startLine: error.locations[0].physicalLocation.region.startLine - 1,
				startCharacter: error.locations[0].physicalLocation.region.startColumn - 1,
				endLine: error.locations[0].physicalLocation.region.endLine - 1,
				endCharacter: error.locations[0].physicalLocation.region.endColumn - 1,
				relatedInformation: undefined,
				source: 'roslyn'
			};
			diagnostics.push(diagnostic);
		}
		return diagnostics;
	}
}
